﻿/* 
 * Copyright 2010 Justin Pealing
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace Minnow.Web.FastCGI
{
    /// <summary>
    /// Enumeration of FastCGI application roles.
    /// </summary>
    public enum Role
    {
        FCGI_RESPONDER = 1,
        FCGI_AUTHORIZER = 2,
        FCGI_FILTER = 3
    }

    /// <summary>
    /// Enumeration of FastCGI record types.
    /// </summary>
    internal enum RecordType
    {
        FCGI_BEGIN_REQUEST = 1,
        FCGI_ABORT_REQUEST = 2,
        FCGI_END_REQUEST = 3,
        FCGI_PARAMS = 4,
        FCGI_STDIN = 5,
        FCGI_STDOUT = 6,
        FCGI_STDERR = 7,
        FCGI_DATA = 8,
        FCGI_GET_VALUES = 9,
        FCGI_GET_VALUES_RESULT = 10,
        FCGI_UNKNOWN_TYPE = 11
    }

    /// <summary>
    /// Enumeration of FastCGI protocol status values.
    /// </summary>
    public enum ProtocolStatus
    {
        /// <summary>
        /// Indicates a normal end of request.
        /// </summary>
        FCGI_REQUEST_COMPLETE = 0,

        /// <summary>
        /// Indicates that the FastCGI application is rejecting a new request. This happens when a Web server sends concurrent 
        /// requests over one connection to an application that is designed to process one request at a time per connection.
        /// </summary>
        FCGI_CANT_MPX_CONN = 1,

        /// <summary>
        /// Indicates that the FastCGI application is rejecting a new request. This happens when the application runs out of 
        /// some resource, e.g. database connections.
        /// </summary>
        FCGI_OVERLOADED = 2,

        /// <summary>
        /// Indicates that the FastCGI application is rejecting a new request. This happens when the Web server has specified 
        /// a role that is unknown to the application.
        /// </summary>
        FCGI_UNKNOWN_ROLE = 3
    }

    /// <summary>
    /// A FastCGI end request record.
    /// </summary>
    internal struct EndRequestBody
    {
        /// <summary>
        /// Initialises a new instance of <see cref="EndRequestBody" /> from the record body.
        /// </summary>
        /// <param name="Data">Record body as a byte array.</param>
        /// <exception cref="ArgumentException">Data length is not 8 bytes.</exception>
        /// <exception cref="ArgumentNullException">Data is null.</exception>
        public EndRequestBody(byte[] Data)
            : this()
        {
            this.AppStatus =
                (Data[0] << 24) +
                (Data[1] << 16) +
                (Data[2] << 8) +
                Data[3];
            this.ProtocolStatus = (ProtocolStatus)Data[4];
        }

        /// <summary>
        /// Gets the role-specific app status.
        /// </summary>
        public int AppStatus;

        /// <summary>
        /// Gets the a protocol-level status code.
        /// </summary>
        public ProtocolStatus ProtocolStatus;
    }

    /// <summary>
    /// Describes a FastCGI record
    /// </summary>
    internal struct Record
    {
        /// <summary>
        /// Initialises a new instance of <see cref="Record" /> from header data.
        /// </summary>
        /// <param name="Header">Header as a byte array.</param>
        /// <exception cref="ArgumentException">Header length is not 8 bytes.</exception>
        /// <exception cref="ArgumentNullException">Header is null.</exception>
        public Record(byte[] Header)
            : this()
        {
            if (Header == null)
            {
                throw new ArgumentNullException("Header");
            }
            if (Header.Length != 8)
            {
                throw new ArgumentException("Header length is not 8");
            }

            this.Version = Header[0];
            this.Type = (RecordType)Header[1];
            this.RequestId = (Header[2] << 8) + Header[3];
            this.ContentLength = (Header[4] << 8) + Header[5];
            this.PaddingLength = Header[6];
        }

        /// <summary>
        /// FastCGI protocol version
        /// </summary>
        public int Version;

        /// <summary>
        /// FastCGI record type
        /// </summary>
        public RecordType Type;

        /// <summary>
        /// Identifies the FastCGI request to which the record belongs
        /// </summary>
        public int RequestId;

        /// <summary>
        /// The number of bytes in the ContentData component of the record
        /// </summary>
        public int ContentLength;

        /// <summary>
        /// The number of bytes in the PaddingData component of the record
        /// </summary>
        public int PaddingLength;

        /// <summary>
        /// Record content data
        /// </summary>
        public byte[] ContentData;

        /// <summary>
        /// Record padding data
        /// </summary>
        public byte[] PaddingData;

        /// <summary>
        /// Returns the header for the record.
        /// </summary>
        /// <returns>Record header as a byte array.</returns>
        public byte[] GetHeader()
        {
            byte[] returnValue = new byte[8];

            returnValue[0] = (byte)this.Version;
            returnValue[1] = (byte)this.Type;
            returnValue[2] = (byte)(this.RequestId >> 8);
            returnValue[3] = (byte)this.RequestId;
            returnValue[4] = (byte)(this.ContentLength >> 8);
            returnValue[5] = (byte)this.ContentLength;
            returnValue[6] = (byte)this.PaddingLength;

            return returnValue;
        }
    }

    /// <summary>
    /// Describes a single FastCGI name-value pair.
    /// </summary>
    internal struct NameValuePair
    {
        /// <summary>
        /// Name of the name-value pair.
        /// </summary>
        public string Name;

        /// <summary>
        /// Value of the name-value pair.
        /// </summary>
        public string Value;

        /// <summary>
        /// Returns the header data for the name-value pair.
        /// </summary>
        /// <returns>Name value pair header as a byte array.</returns>
        public byte[] GetHeader()
        {
            // TODO: Encoding lengths in less than 4 bytes
            // TODO: Validation of string length
            int nameLength = this.Name.Length;
            int valueLength = this.Value == null ? 0 : this.Value.Length;
            byte[] returnValue = new byte[8];

            returnValue[0] = (byte)((nameLength << 24) | (1 << 7));
            returnValue[1] = (byte)(nameLength << 16);
            returnValue[2] = (byte)(nameLength << 8);
            returnValue[3] = (byte)nameLength;
            returnValue[4] = (byte)((valueLength << 24) | (1 << 7));
            returnValue[5] = (byte)(valueLength << 16);
            returnValue[6] = (byte)(valueLength << 8);
            returnValue[7] = (byte)valueLength;

            return returnValue;
        }
    }

    /// <summary>
    /// Describes the body of a FastCGI begin request record.
    /// </summary>
    internal struct BeginRequestBody
    {
        private const int FCGI_KEEP_CONN = 1;

        /// <summary>
        /// The role the Web server expects the application to play.
        /// </summary>
        public Role Role;

        /// <summary>If false, the FastCGI application closes the connection after responding 
        /// to this request.  If true, the child FastCGI application does not close the connection after responding 
        /// to this request; the Web server retains responsibility for the connection.</summary>
        public bool KeepConnection;

        /// <summary>
        /// Returns the record body.
        /// </summary>
        /// <returns>Record body as a byte array.</returns>
        public byte[] ToByteArray()
        {
            byte[] returnValue = new byte[8];

            returnValue[1] = (byte)this.Role;
            byte flags = 0;
            if (this.KeepConnection)
            {
                flags |= FCGI_KEEP_CONN;
            }
            returnValue[2] = flags;

            return returnValue;
        }
    }

    /// <summary>
    /// Deals with the low-level tasks of sending and recieving FastCGI messages
    /// </summary>
    internal class Connection
    {
        private object readLock = new object();
        private const int FCGI_VERSION_1 = 1;

        /// <summary>
        /// Initialises an instance of <see cref="Connection" /> for a new connection.
        /// </summary>
        /// <param name="Port">The port to connect to.</param>
        public Connection(int Port)
        {
            this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            this.Socket.Connect(IPAddress.Loopback, Port);
        }

        private delegate Record ReadRecordDelegate();
        private ReadRecordDelegate readRecordDelegate;

        /// <summary>
        /// Gets and sets the underlying socket
        /// </summary>
        public Socket Socket
        {
            get
            {
                // TODO: Buffering
                return this.socket;
            }
        }
        private Socket socket;

        /// <summary>
        /// Sends a begin request record to the FastCGI client application.
        /// </summary>
        /// <param name="RequestId">Identifies the FastCGI request to which the record belongs.</param> 
        /// <param name="Role">The role the Web server expects the application to play.</param>
        /// <param name="KeepConnection">If false, the FastCGI application closes the connection after responding 
        /// to this request.  If true, the child FastCGI application does not close the connection after responding 
        /// to this request; the Web server retains responsibility for the connection.</param>
        public void BeginRequest(int RequestId, Role Role, bool KeepConnection)
        {
            BeginRequestBody request = new BeginRequestBody()
                {
                    Role = Role,
                    KeepConnection = KeepConnection
                };
            byte[] bytes = request.ToByteArray();
            this.SendRecord(RequestId, RecordType.FCGI_BEGIN_REQUEST, bytes, 0, bytes.Length);
        }

        /// <summary>
        /// Sends a collection of name-value pair to the FastCGI child process.
        /// </summary>
        /// <param name="RequestId">Identifies the FastCGI request to which the record belongs.</param>
        /// <param name="Params">Parameters to send, or null to send an empty params record.</param>
        public void SendParams(int RequestId, IEnumerable<KeyValuePair<string, string>> Params)
        {
            if (Params == null)
            {
                this.SendRecord(RequestId, RecordType.FCGI_PARAMS);
            }
            else
            {
                // TODO: Better handling of null environment variables (keys and values)
                MemoryStream builder = new MemoryStream();
                foreach (KeyValuePair<string, string> param in Params)
                {
                    NameValuePair paramBody = new NameValuePair()
                        {
                            Name = param.Key,
                            Value = param.Value
                        };
                    builder.Write(paramBody.GetHeader(), 0, 8);
                    builder.Write(Encoding.ASCII.GetBytes(param.Key), 0, param.Key.Length);
                    if (param.Value != null)
                    {
                        builder.Write(Encoding.ASCII.GetBytes(param.Value), 0, param.Value.Length);
                    }
                }
                byte[] bytes = builder.ToArray();

                this.SendRecord(RequestId, RecordType.FCGI_PARAMS, bytes, 0, bytes.Length);
            }
        }

        /// <summary>
        /// Sends an abort request record to the FastCGI child process.
        /// </summary>
        /// <param name="RequestId">Identifies the FastCGI request to which the record belongs.</param>
        public void AbortRequest(int RequestId)
        {
            this.SendRecord(RequestId, RecordType.FCGI_ABORT_REQUEST);
        }

        /// <summary>
        /// Reads a record from the FastCGI child process.
        /// </summary>
        public Record ReadRecord()
        {
            lock (this.readLock)
            {
                Record record = new Record(this.ReadData(8));
                if (record.ContentLength > 0)
                {
                    record.ContentData = this.ReadData(record.ContentLength);
                }
                if (record.PaddingLength > 0)
                {
                    record.PaddingData = this.ReadData(record.PaddingLength);
                }
                return record;
            }
        }

        public IAsyncResult BeginReadRecord(AsyncCallback Callback, object State)
        {
            // TODO: This is definitely not thread safe
            ReadRecordDelegate readRecordDelegate = new ReadRecordDelegate(this.ReadRecord);
            this.readRecordDelegate = readRecordDelegate;
            return readRecordDelegate.BeginInvoke(Callback, State);
        }

        public Record EndReadRecord(IAsyncResult Result)
        {
            // TODO: This is definitely not thread safe
            return this.readRecordDelegate.EndInvoke(Result);
        }

        /// <summary>
        /// Sends a record with no data to the FastCGI child process.
        /// </summary>
        /// <param name="RequestId">Identifies the FastCGI request to which the record belongs.</param>
        /// <param name="RecordType">The type of record to send to the child process.</param>
        public void SendRecord(int RequestId, RecordType RecordType)
        {
            this.SendRecord(RequestId, RecordType, null, 0, 0);
        }

        /// <summary>
        /// Sends a record to the FastCGI child process.
        /// </summary>
        /// <param name="RequestId">Identifies the FastCGI request to which the record belongs.</param>
        /// <param name="RecordType">The type of record to send to the child process.</param>
        /// <param name="Buffer">An array of type <see cref="Byte" /> that contains the data to send.</param>
        /// <param name="Offset">The location in Buffer from which to start writing data.</param>
        /// <param name="Count">The number of bytes to send.</param>
        public void SendRecord(int RequestId, RecordType RecordType, byte[] Buffer, int Offset, int Count)
        {
            if (Buffer != null)
            {
                if (Offset < 0 || Count < 0 || Offset + Count > Buffer.Length)
                {
                    throw new ArgumentOutOfRangeException();
                }   
            }

            // TODO: Padding
            Record record = new Record();
            record.ContentLength = Count;
            record.PaddingLength = 0;
            record.RequestId = RequestId;
            record.Type = RecordType;
            record.Version = FCGI_VERSION_1;

            MemoryStream data = new MemoryStream();
            data.Write(record.GetHeader(), 0, 8);
            if (Buffer != null)
            {
                data.Write(Buffer, Offset, Count);
            }
            this.Socket.Send(data.ToArray());
        }

        /// <summary>
        /// Reads data from the FastCGI child process.
        /// </summary>
        /// <remarks>
        /// Gaurenteed to return a byte array of exactly the length requested.
        /// </remarks>
        /// <param name="Length">Length of the data to read.</param>
        /// <returns>Data as a byte array.</returns>
        private byte[] ReadData(int Length)
        {
            byte[] returnValue = new byte[Length];
            int bytesRead = 0;
            
            while (bytesRead < Length)
            {
                bytesRead += this.Socket.Receive(returnValue, bytesRead, Length - bytesRead, SocketFlags.None);
            }

            return returnValue;
        }
    }
}
